/*
 * Copyright (C) 2009 JavaRosa
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.opendatakit.collect.android.logic;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.NoSuchPaddingException;

import org.apache.commons.lang.StringEscapeUtils;
import org.opendatakit.collect.android.application.Collect;
import org.opendatakit.collect.android.provider.InstanceValueProvider;
import org.opendatakit.collect.android.utilities.EncryptionUtils;
import org.opendatakit.collect.android.utilities.EncryptionUtils.CombinedEncryptedFormInformation;
import org.opendatakit.collect.android.utilities.EncryptionUtils.EncryptedFormInformation;
import org.opendatakit.collect.android.utilities.FileUtils;

/**
 * This class is a wrapper for Javarosa's FormEntryController. In theory, if you wanted to replace
 * javarosa as the form engine, you should only need to replace the methods in this file. Also, we
 * haven't wrapped every method provided by FormEntryController, only the ones we've needed so far.
 * Feel free to add more as necessary.
 * 
 * @author carlhartung
 */
public class FormController {

    private static final String t = "FormController";
    private File mMediaFolder;
    private Map<String,String> mInstanceMap;
    private String mCurrentLanguage = "en";

    public String mInstanceId;
    
    public static final boolean STEP_INTO_GROUP = true;
    public static final boolean STEP_OVER_GROUP = false;

    /**
     * OpenRosa metadata of a form instance.
     * 
     * Contains the values for the required metadata
     * fields and nothing else.
     * 
     * @author mitchellsundt@gmail.com
     *
     */
    public static final class InstanceMetadata {
        public final String instanceId;
        public final String base64EncryptedFileRsaPublicKey;
        public final String base64EncryptedFieldRsaPublicKey;
    	
        InstanceMetadata( String instanceId, 
        					String base64EncryptedFileRsaPublicKey, 
        					String base64EncryptedFieldRsaPublicKey ) {
            this.instanceId = instanceId;
            this.base64EncryptedFileRsaPublicKey = base64EncryptedFileRsaPublicKey;
            this.base64EncryptedFieldRsaPublicKey = base64EncryptedFieldRsaPublicKey;
        }
    };


    public FormController(File mediaFolder, String instanceId) {
    	mInstanceId = instanceId;
    	mMediaFolder = mediaFolder;
    	String name = mMediaFolder.getName();
    	name = name.substring(0, name.lastIndexOf("-media")) + ".xml";
    	mInstanceMap = FileUtils.parseXML(new File(mMediaFolder.getParent(), name));
    }

    public File getMediaFolder() {
    	return mMediaFolder;
    }


    /**
     * Return the langauges supported by the currently loaded form.
     * 
     * @return Array of Strings containing the languages embedded in the XForm.
     */
    public String[] getLanguages() {
    	return new String[] { mCurrentLanguage };
    }

    public String getFormId() {
    	return mInstanceMap.get(FileUtils.FORMID);
    }
    
    public String getVersion() {
    	return mInstanceMap.get(FileUtils.VERSION);
    }

    /**
     * @return A String containing the title of the current form.
     */
    public String getFormTitle() {
    	return mInstanceMap.get(FileUtils.TITLE);
    }


    /**
     * @return the currently selected language.
     */
    public String getLanguage() {
        return mCurrentLanguage;
    }

 
    public byte[] getInstanceXml(boolean finalized) {
    	return new byte[] {};
    }

    /**
     * Sets the current language.
     * 
     * @param language
     */
    public void setLanguage(String language) {
    	if ( Arrays.asList(getLanguages()).contains(language) ) {
    		mCurrentLanguage = language;
    	} else {
    		throw new IllegalArgumentException("Requested language is not among those supported: " + language);
    	}
    }

   
    /**
     * Once a submission is marked as complete, it is saved in the 
     * submission format, which might be a fragment of the original
     * form or might be a SMS text string, etc.
     * 
     * @return true if the submission is the entire form.  If it is, 
     *              then the submission can be re-opened for editing
     *              after it was marked-as-complete (provided it has
     *              not been encrypted).
     */
    public boolean isSubmissionEntireForm() {
    	return mInstanceMap.get(FileUtils.SUBMISSIONREF).equals(mInstanceMap.get(FileUtils.ROOTREF));
    }
    
    public CombinedEncryptedFormInformation getEncryptedFormInformation() {
    	// see if the form is encrypted and we can encrypt it...
        CombinedEncryptedFormInformation formInfo = EncryptionUtils.getEncryptedFormInformation(this);
        return formInfo;

    }
    public void saveAllValues(boolean isComplete) {

    	InstanceValueProvider db = Collect.getInstance().getIvProvider();
    	
    	CombinedEncryptedFormInformation formInfo = getEncryptedFormInformation();
    	// move the values into the saved-values table...
    	EncryptedFormInformation encryptionInfo = (formInfo == null) ? null : formInfo.encryptedFields;
    	
    	String instanceIdRef = mInstanceMap.get(FileUtils.INSTANCEIDREF);
    	if ( instanceIdRef != null ) {
	    	db.assertValue(mInstanceMap.get(FileUtils.FORMID), mInstanceMap.get(FileUtils.VERSION), 
	    			mInstanceId, 
	    			instanceIdRef, mInstanceId);
    	}

        if ( encryptionInfo != null && mInstanceMap.get(FileUtils.FIELDKEYREF) != null ) {
        	
        	db.assertSavedValue(mInstanceMap.get(FileUtils.FORMID), mInstanceMap.get(FileUtils.VERSION), 
        			mInstanceId, 
        			mInstanceMap.get(FileUtils.FIELDKEYREF), encryptionInfo.base64RsaEncryptedSymmetricKey);
        }

        db.saveAllValues(mInstanceMap.get(FileUtils.FORMID), mInstanceMap.get(FileUtils.VERSION), mInstanceId);
    }
    
    /**
     * Extract the portion of the form that should be uploaded to the server.
     * 
     * @return
     * @throws IOException
     */
    public byte[] getSubmissionXml(EncryptedFormInformation encryptionInfo) throws IOException {

    	InstanceValueProvider db = Collect.getInstance().getIvProvider();
    	
    	String instanceIdRef = mInstanceMap.get(FileUtils.INSTANCEIDREF);
    	if ( instanceIdRef != null ) {
	    	db.assertSavedValue(mInstanceMap.get(FileUtils.FORMID), mInstanceMap.get(FileUtils.VERSION), 
	    			mInstanceId, 
	    			instanceIdRef, mInstanceId);
    	}
    	
        if ( encryptionInfo != null ) {
        	
        	db.assertSavedValue(mInstanceMap.get(FileUtils.FORMID), mInstanceMap.get(FileUtils.VERSION), 
        			mInstanceId, 
        			mInstanceMap.get(FileUtils.FIELDKEYREF), encryptionInfo.base64RsaEncryptedSymmetricKey);
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        serialize( bos, 0, db, encryptionInfo );
        
        byte[] payload =  bos.toByteArray();
        return payload;
    }
    
    private void emit(ByteArrayOutputStream bos, String value) {
		try {
			bos.write(value.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
    }
    
    private int serialize(ByteArrayOutputStream bos, int termCount, InstanceValueProvider db, EncryptedFormInformation encryptionInfo ) {
    	String ref = mInstanceMap.get( FileUtils.SUBMISSIONREF + 
    			(( termCount == 0 ) ? "" : Integer.toString(-termCount)) );
    	String key;
		String next = mInstanceMap.get(FileUtils.SUBMISSIONREF + Integer.toString(-(termCount+1)));
		boolean isLeaf = (next == null) || !next.startsWith(ref);
    	if ( termCount == 0 ) {
    		String[] elems = ref.split("/");
    		key = elems[elems.length-1];
    		// start of form
    		emit(bos, "<?xml version=\"1.0\" encoding=\"utf-8\"?>");
    		
    		String version = mInstanceMap.get(FileUtils.VERSION);
    		emit(bos, "<" + key + " id=\"" + StringEscapeUtils.escapeXml(mInstanceMap.get(FileUtils.FORMID)) + "\"" +
    				((version == null) ? "" : (" version=\"" + StringEscapeUtils.escapeXml(version) + "\"")) + ">");
    	} else {
    		// determine whether there are nested elements or if this is a leaf element.
    		String[] elems = ref.split("/");
    		key = elems[elems.length-1];
    		if ( isLeaf ) {
    			String value = db.getSavedValue( mInstanceMap.get(FileUtils.FORMID), mInstanceMap.get(FileUtils.VERSION), 
    									mInstanceId, ref);
    			if ( value == null ) {
    				// optimization: omit null values...
    				return termCount + 1;
    			}
    			
    			String encrypted = mInstanceMap.get(FileUtils.ENCRYPTEDREF+ref);
    			if ( encrypted != null ) {
					// construct encrypted string
					Cipher c;
					try {
						c = encryptionInfo.getCipher(key);
						ByteArrayOutputStream fba;
						OutputStream fout;
						fba = new ByteArrayOutputStream(256);
						fout = new CipherOutputStream(fba, c);
						fout.write(value.getBytes("UTF-8"));
						fout.flush();
						fout.close();
						value = encryptionInfo.encodeBase64(fba.toByteArray()); 
					} catch (InvalidKeyException e1) {
						e1.printStackTrace();
					} catch (InvalidAlgorithmParameterException e1) {
						e1.printStackTrace();
					} catch (NoSuchAlgorithmException e1) {
						e1.printStackTrace();
					} catch (NoSuchPaddingException e1) {
						e1.printStackTrace();
					} catch (IOException e1) {
						e1.printStackTrace();
					}
    			}
    			emit(bos, "<" + key + ">" + StringEscapeUtils.escapeXml(value) + "</" + key + ">");
    			return termCount + 1;
    		}
    	}

    	while ( !isLeaf ) {
    		termCount = serialize(bos, termCount+1, db, encryptionInfo);
    		next = mInstanceMap.get(FileUtils.SUBMISSIONREF + Integer.toString(-(termCount+1)));
    		isLeaf = (next == null) || !next.startsWith(ref);
    	}
    	emit(bos, "</" + key + ">");
    	return termCount;
    }

    /**
     * Get the OpenRosa required metadata of the portion of the form beng submitted
     * @return
     */
    public InstanceMetadata getSubmissionMetadata() {
        String instanceId = mInstanceId;
        String base64EncryptedFileRsaPublicKey = null;
        String base64EncryptedFieldRsaPublicKey = null;
        
    	base64EncryptedFieldRsaPublicKey = mInstanceMap.get(FileUtils.BASE64_ENCRYPTED_FIELD_RSA_PUBLIC_KEY); 
    	if ( base64EncryptedFieldRsaPublicKey != null ) {
    		base64EncryptedFieldRsaPublicKey = base64EncryptedFieldRsaPublicKey.trim();
        	if ( base64EncryptedFieldRsaPublicKey.length() == 0 ) {
        		base64EncryptedFieldRsaPublicKey = null;
        	}
    	}
        
        // Determine the information about the submission...
    	base64EncryptedFileRsaPublicKey = mInstanceMap.get(FileUtils.BASE64_RSA_PUBLIC_KEY);
    	if ( base64EncryptedFileRsaPublicKey != null ) {
    		base64EncryptedFileRsaPublicKey = base64EncryptedFileRsaPublicKey.trim();
        	if ( base64EncryptedFileRsaPublicKey.trim().length() == 0 ) {
        		base64EncryptedFileRsaPublicKey = null;
        	}
    	}
        
        return new InstanceMetadata(instanceId, base64EncryptedFileRsaPublicKey, base64EncryptedFieldRsaPublicKey);
    }

}
